package joc.internal.transformer;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import joc.JOCEnabled;
import joc.customize.Contract;
import joc.customize.ContractRetainingState;

import org.apache.log4j.Logger;

public abstract class ClassTransformer {
	protected Logger logger = Logger.getLogger(getClass());
	protected ClassPool pool;

	public ClassTransformer(ClassPool pool) {
		this.pool = pool;
	}

	public abstract void transform(CtClass clazz) throws Exception;

	public List<Method> getContractMethodsWithAnnotation(Contract contract, Class<?>... annotationClasses) {
		List<Method> methods = new ArrayList<Method>();
		for (Method contractMethod : contract.getClass().getMethods()) {
			for (Class<?> annotationClass : annotationClasses) {
				@SuppressWarnings("unchecked")
				Class<? extends Annotation> annotationClassCasted = (Class<? extends Annotation>) annotationClass;
				Annotation methodAnnotation = contractMethod.getAnnotation(annotationClassCasted);
				if (methodAnnotation != null) {
					methods.add(contractMethod);
				}
			}
		}
		return methods;
	}

	public String getContractCallString(Method contractMethod, Contract contract, Annotation annotation,
			boolean evaluate, String... moreArgs) throws Exception {
		return getContractCallString(null, false, contractMethod, contract, annotation, evaluate, moreArgs);
	}

	public String getContractCallString(CtBehavior behavior, boolean initialize, Method contractMethod,
			Contract contract, Annotation annotation, boolean evaluate, String... moreArgs) throws Exception {
		Map<String, String> annotationValues = new HashMap<String, String>();
		logger.info("creating callString for method " + contractMethod.getName() + " and annotation "
				+ contract.getAnnotationClass().getName());
		for (Method annotationMethod : contract.getAnnotationClass().getDeclaredMethods()) {
			annotationValues
					.put(annotationMethod.getName(), getAnnotationValue(annotation, annotationMethod.getName()));
		}
		List<String> sortedAnnotationValueKeys = new ArrayList<String>(annotationValues.keySet());
		Collections.sort(sortedAnnotationValueKeys);
		List<String> sortedAnnotationValues = new ArrayList<String>();
		for (String key : sortedAnnotationValueKeys) {
			sortedAnnotationValues.add("joc.internal.ObjectConverter#toObject(" + annotationValues.get(key) + ")");
		}
		String callString = "";
		String contractInstance = "(new " + contract.getClass().getName() + "())";
		if (contract instanceof ContractRetainingState) {
			String quotedContractIdentifier = "\"" + behavior.getLongName() + "@"
					+ contract.getAnnotationClass().getName() + "\"";
			if (initialize) {
				callString += "joc.internal.Cache#saveCondition(this, " + quotedContractIdentifier + ", "
						+ contractInstance + "); ";
			}
			contractInstance = "((" + contract.getClass().getName() + ") joc.internal.Cache#getCondition(this, "
					+ quotedContractIdentifier + "))";
		}
		if (evaluate) {
			callString += "joc.internal.Behavior#evaluateContract(";
		}
		callString += contractInstance + "." + contractMethod.getName() + "(";
		callString += "(" + contract.getAnnotationClass().getName()
				+ ") joc.internal.util.AnnotationInstantiator#getAnnotationArrayAccessible("
				+ contract.getAnnotationClass().getName() + ".class";
		if (!sortedAnnotationValues.isEmpty()) {
			callString += ", new Object[] {" + joinString(sortedAnnotationValues.toArray(new String[] {}), ",", false)
					+ "}";
		}
		callString += ")" + joinString(moreArgs, ", ", true);
		callString += ")";
		if (evaluate) {
			callString += ")";
		}
		callString += ";";
		logger.info("callString: " + callString);
		return callString;
	}

	private String getAnnotationValue(Annotation annotation, String name) throws Exception {
		logger.info("invoking method " + name + " on annotation " + annotation.getClass().getName());
		return getValueAsString(annotation.getClass().getMethod(name).invoke(annotation), annotation.getClass()
				.getMethod(name).getReturnType());
	}

	protected String getValueAsString(Object value, Class<?> targetClass) {
		if (value instanceof Class) {
			return getValueAsString((Class<?>) value);
		}
		if (value instanceof String) {
			return getValueAsString((String) value);
		}
		if (value instanceof Enum) {
			return getValueAsString((Enum<?>) value);
		}
		if (value instanceof Object[]) {
			return getValueAsString((Object[]) value, targetClass);
		}
		if (targetClass.isPrimitive()) {
			return "(" + targetClass.getName() + ") " + value.toString();
		}
		return value.toString();
	}

	private String getValueAsString(Class<?> value) {
		return value.getName() + ".class";
	}

	private String getValueAsString(String value) {
		return '"' + value + '"';
	}

	private String getValueAsString(Enum<?> value) {
		return value.getClass().getName() + "." + value.name();
	}

	private String getValueAsString(Object[] values, Class<?> targetClass) {
		if (values.length == 0) {
			return "new " + targetClass.getComponentType().getCanonicalName() + "[0]";
		}
		StringBuilder result = new StringBuilder("new " + targetClass.getComponentType().getCanonicalName() + "[] {");
		boolean first = true;
		for (Object value : values) {
			if (!first) {
				result.append(",");
			}
			result.append(getValueAsString(value, targetClass.getComponentType()));
			first = false;
		}
		result.append("}");
		return result.toString();
	}

	protected String joinString(String[] parts, String delimiter, boolean includeFirstDelimiter) {
		StringBuilder sb = new StringBuilder();
		boolean first = true;
		for (String part : parts) {
			if (!first || includeFirstDelimiter) {
				sb.append(delimiter);
			}
			sb.append(part);
			first = false;
		}
		return sb.toString();
	}

	protected void insertBefore(CtBehavior behavior, String code) throws CannotCompileException {
		if (behavior instanceof CtConstructor) {
			((CtConstructor) behavior).insertBeforeBody("{ " + code + " }");
		} else {
			behavior.insertBefore("{ " + code + " }");
		}
	}

	protected void insertAfter(CtBehavior behavior, String code) throws CannotCompileException {
		behavior.insertAfter("{" + code + "}");
	}

	protected String handlePrimitive(CtClass clazz, String code) {
		if (clazz.isPrimitive()) {
			return "joc.internal.ObjectConverter.toPrimitive(" + code + ")";
		}
		return "joc.internal.ObjectConverter.toObject(" + code + ")";
	}

	protected String handlePrimitive(Class<?> clazz, String code) {
		if (clazz.isPrimitive()) {
			return "joc.internal.ObjectConverter.toPrimitive(" + code + ")";
		}
		return "joc.internal.ObjectConverter.toObject(" + code + ")";
	}

	protected List<CtBehavior> getConsultedBehaviors(CtBehavior behavior) throws NotFoundException {
		List<CtBehavior> behaviors = new ArrayList<CtBehavior>(Collections.singleton(behavior));
		logger.debug("behavior: " + behavior + " with declaring class " + behavior.getDeclaringClass().getName());
		List<CtClass> consultedClasses = getConsultedTypes(behavior.getDeclaringClass());
		for (CtClass consultedClass : consultedClasses) {
			logger.debug("consulted class: " + consultedClass.getName());
			for (CtBehavior superBehavior : consultedClass.getDeclaredBehaviors()) {
				if (isOverridden(behavior, superBehavior)) {
					logger.info("adding superBehavior: " + superBehavior + " with declaring class "
							+ superBehavior.getDeclaringClass().getName());
					behaviors.add(superBehavior);
				}
			}
		}
		return behaviors;
	}

	protected List<CtClass> getConsultedTypes(CtClass clazz) throws NotFoundException {
		List<CtClass> consultedClasses = new ArrayList<CtClass>();
		consultedClasses.addAll(getConsultedImmediateInterfaces(clazz));
		CtClass superClass = clazz.getSuperclass();
		while (superClass != null) {
			addConsultable(consultedClasses, superClass);
			consultedClasses.addAll(getConsultedImmediateInterfaces(superClass));
			superClass = superClass.getSuperclass();
		}
		return consultedClasses;
	}

	private List<CtClass> getConsultedImmediateInterfaces(CtClass clazz) throws NotFoundException {
		List<CtClass> consultedInterfaces = new ArrayList<CtClass>();
		for (CtClass interfaze : clazz.getInterfaces()) {
			addConsultable(consultedInterfaces, interfaze);
		}
		return consultedInterfaces;
	}

	private void addConsultable(List<CtClass> consultedClasses, CtClass clazz) {
		if (clazz.hasAnnotation(JOCEnabled.class)) {
			consultedClasses.add(clazz);
		}
	}

	private boolean isOverridden(CtBehavior behavior, CtBehavior superBehavior) throws NotFoundException {
		logger.debug(behavior.getLongName() + " --- " + superBehavior.getLongName());
		if (!behavior.getClass().equals(superBehavior.getClass())) {
			logger.debug("don't have the same type");
			return false;
		}
		if (behavior instanceof CtMethod) {
			return behavior.equals(superBehavior);
		}
		if (behavior.getParameterTypes().length != superBehavior.getParameterTypes().length) {
			logger.debug("not the same amount of parameters");
			return false;
		}
		for (int i = 0; i < behavior.getParameterTypes().length; i++) {
			if (!behavior.getParameterTypes()[i].equals(superBehavior.getParameterTypes()[i])) {
				logger.debug("parameter " + i + " has different type");
				return false;
			}
		}
		if (!isSubclass(behavior.getDeclaringClass(), superBehavior.getDeclaringClass())) {
			logger.debug("not in a superclass");
			return false;
		}
		if (!isSameOrLessRestrictive(behavior.getModifiers(), superBehavior.getModifiers())) {
			logger.debug("not less restrictive");
			return false;
		}
		if (Modifier.isPrivate(superBehavior.getModifiers()) || Modifier.isFinal(superBehavior.getModifiers())
				|| Modifier.isStatic(superBehavior.getModifiers())) {
			logger.debug("superBehavior has invalid modifiers");
			return false;
		}
		if (Modifier.isPrivate(behavior.getModifiers()) || Modifier.isStatic(behavior.getModifiers())) {
			logger.debug("behavior has invalid modifiers");
			return false;
		}
		if (Modifier.isPackage(superBehavior.getModifiers())
				&& !behavior.getDeclaringClass().getPackageName()
						.equals(superBehavior.getDeclaringClass().getPackageName())) {
			logger.debug("package visibility broken");
			return false;
		}
		return true;
	}

	private boolean isSameOrLessRestrictive(int modifiers, int superModifiers) {
		if (Modifier.isPublic(modifiers)) {
			return Modifier.isPublic(superModifiers) || Modifier.isProtected(superModifiers)
					|| Modifier.isPackage(superModifiers);
		}
		if (Modifier.isProtected(modifiers)) {
			return Modifier.isProtected(superModifiers) || Modifier.isPackage(superModifiers);
		}
		if (Modifier.isPackage(modifiers)) {
			return Modifier.isPackage(superModifiers);
		}
		return false;
	}

	private boolean isSubclass(CtClass clazz, CtClass superClass) throws NotFoundException {
		if (clazz.getSuperclass() == null) {
			return false;
		}
		if (clazz.getSuperclass().equals(superClass)) {
			return true;
		}
		return isSubclass(clazz.getSuperclass(), superClass);
	}

	protected String getParamsForConditionMethod(CtBehavior behavior, CtMethod pcMethod, CtClass returnType)
			throws NotFoundException {
		if (pcMethod.getParameterTypes().length == 0) {
			return "";
		}
		String params = "this";
		int allowedNumParams = 1;
		if (behavior != null) {
			allowedNumParams = behavior.getParameterTypes().length + 1;
		}
		int paramsOffset = 1;
		if (returnType != null && pcMethod.getParameterTypes().length > 1) {
			allowedNumParams++;
			paramsOffset++;
			params += ", " + handlePrimitive(pcMethod.getParameterTypes()[1], "$_");
		}
		if (pcMethod.getParameterTypes().length > allowedNumParams) {
			throw new JOCTransformationException("method " + pcMethod.getLongName() + " can have no more than "
					+ allowedNumParams + " arguments");
		}
		for (int i = 1; i <= pcMethod.getParameterTypes().length - paramsOffset; i++) {
			params += ", " + handlePrimitive(pcMethod.getParameterTypes()[i - 1 + paramsOffset], "$" + i);
		}
		return params;
	}

	protected String callConditionString(CtMethod pcMethod, boolean evaluate) throws NotFoundException {
		return callConditionString(pcMethod, evaluate, null, false);
	}

	protected String callConditionString(CtMethod pcMethod, boolean evaluate, CtBehavior behavior, boolean useReturnType)
			throws NotFoundException {
		CtClass returnType = null;
		if (useReturnType) {
			returnType = getReturnType(behavior);
		}
		String params = getParamsForConditionMethod(behavior, pcMethod, returnType);
		String pcClassName = pcMethod.getDeclaringClass().getName();
		StringBuilder builder = new StringBuilder();
		builder.append("if (joc.internal.Cache#isConditionActive(this, \"").append(pcClassName).append("\")) { ");
		builder.append("joc.internal.Cache#deactivateCondition(this, \"").append(pcClassName).append("\"); ");
		if (evaluate) {
			builder.append("joc.internal.Behavior#evaluateContract(");
		}
		builder.append("((").append(pcClassName).append(") joc.internal.Cache#getCondition(this, \"")
				.append(pcClassName).append("\")).").append(pcMethod.getName()).append("(").append(params).append(")");
		if (evaluate) {
			builder.append(")");
		}
		builder.append("; ");
		builder.append("joc.internal.Cache#activateCondition(this, \"").append(pcClassName).append("\"); ");
		builder.append("} ");
		return builder.toString();
	}

	protected CtClass getReturnType(CtBehavior behavior) throws NotFoundException {
		CtClass returnType = null;
		if (behavior != null && behavior instanceof CtMethod) {
			CtMethod method = (CtMethod) behavior;
			if (!method.getReturnType().equals(CtClass.voidType)) {
				returnType = ((CtMethod) behavior).getReturnType();
			}
		}
		return returnType;
	}

	protected String initializeConditionString(String pcClassName) {
		String pcNameQuoted = "\"" + pcClassName + "\"";
		String insertBefore = "if (!joc.internal.Cache#containsCondition(this, " + pcNameQuoted + ")) { ";
		insertBefore += "joc.internal.Cache#saveCondition(this, " + pcNameQuoted + ", new " + pcClassName + "()); ";
		insertBefore += "} ";
		return insertBefore;
	}
}
